package jkadx.MPrimeFinder;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
import jkadx.networking.NetworkingInterface;
import jkadx.networking.SocketController;
import jkadx.core.CoreConfig;
import jkadx.core.RPC;
import jkadx.dht.Contact;
import jkadx.dht.DHT;
import jkadx.dht.KBucket;
import jkadx.dht.UniqueId;
import jkadx.xml.ElementNode;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedList;

public class MPrimeFinder implements NetworkingInterface
{
    private final static int LOWER = 0;
    private final static int UPPER = 1;
    private           ArrayList<ElementNode>           settings;
    private         CoreConfig                       coreSettings;
    static final byte   FIRST_CHARACTER_RANGE = 32;//32;
    static final byte   LAST_CHARACTER_RANGE  = 126;//126;
    static final BigInteger BigIntegerFOUR = new BigInteger("4");
    static final String ALGORITHM             = "SHA1";
           volatile byte[]       successArray = new byte[0];
           String       successResult;
           int          currentIteration      = 0;
    private      SocketController controller;
    private           int                              incomingPort;
    private           int                              outgoingPort;      
    final String ONE           = "1";
    final String ZERO          = "0";
    FileInputStream  fis;
    UniqueId myUniqueId;
    final Contact you;
    final int coreIncomingPort;
    private byte[]                        collisionPingHeader;
    private byte[]                        workUnitRequestHeader;
    private byte[]                        mPrimeWorkUnitHeader;
    //private byte[]                        collisionFoundHeader;
    private byte[]                        collisionStopHeader;
    ArrayList<Contact>                    contactsList;
    AtomicBoolean isWorking                     = new AtomicBoolean(false);
    AtomicBoolean isServer                      = new AtomicBoolean(false);
    final Charset  CHARSET     = Charset.forName("UTF-8");
    byte[] startArray;
    final Object rangeBoundLock = new Object();
    volatile int workChunkSize;
    int[] exps;
    int[] initialRange = {0, -1};
    int[] currentRange;
    AtomicBoolean first = new AtomicBoolean(true);

    LinkedList<Integer> results;


    public MPrimeFinder() throws Exception
    {
        incomingPort = 60001;
        coreSettings = new CoreConfig("settings", "contacts", -1, -1);
        settings     = coreSettings.getSettings();
        you          = new Contact(myUniqueId = new UniqueId(ElementNode.getDataFor("thisNodeId", settings)),
                       InetAddress.getByName(ElementNode.getDataFor("externalIpAddress", settings)).getAddress(),
                       Integer.parseInt(ElementNode.getDataFor("incomingPort", settings)));
        coreIncomingPort = you.port;

        byte[] bytes           = new byte[RPC.HEADER_LENGTH/* + (UniqueId.NBITS / Byte.SIZE)*/];
        // add my contact information to it (after the flag bits byte)
        you.insert(bytes, RPC.SOURCE_ADDRESS_OFFSET);
        // add the correct flag bits to each rpc
        bytes[0]        = (byte)(RPC.PING_FLAG | RPC.MPRIMEFINDER);
        collisionPingHeader      = bytes.clone();
        bytes[0]        = (byte)(RPC.FIND_NODE_FLAG | RPC.MPRIMEFINDER); //
        workUnitRequestHeader   = bytes.clone();
        bytes[0]        = (byte)(RPC.PING_FLAG | RPC.FIND_NODE_FLAG | RPC.MPRIMEFINDER);
        mPrimeWorkUnitHeader   = bytes.clone();
        bytes[0]        = (byte)(RPC.FIND_VALUE_FLAG | RPC.MPRIMEFINDER);
       // collisionFoundHeader = bytes.clone();
        bytes[0]        = (byte)(RPC.STORE_FLAG | RPC.MPRIMEFINDER);
        collisionStopHeader   = bytes.clone();
    }


    public void commandLine() throws Exception
    {
        Scanner in     = new Scanner(System.in);

        System.out.println("\nMPrimeFinder Command Line Interface.  Type 'p' to Print menu.");
        while(true)
        {
            String input = null;
            System.out.print("> ");
            input = in.nextLine();
            if (input.equalsIgnoreCase("P"))
            {
                System.out.println("The following commands are not case sensitive:\n");
                System.out.println("(P)rint this menu");
                System.out.println("(G)et contacts list");
                System.out.println("(E)nter ports");
                System.out.println("(R)ange for finding Mersenne Primes");
                if (!this.isServer.get())
                {
                    System.out.println("(F)ind Mersenne Primes");
                }
                System.out.println("*(A)bout this application");
                if (!this.isServer.get())
                {
                    System.out.println("(S)hutdown");
                }
                else
                {
                    System.out.println("(S)top current job");
                }
                System.out.println();
            }
            else if (input.equalsIgnoreCase("F"))
            {
                if (!this.isServer.get())
                {
                    if (contactsList == null || contactsList.size() == 0)
                    {
                        System.out.println("You must get a list of contacts first.");
                    }
                    else if (this.initialRange[UPPER] < 0)
                    {
                        System.out.println("You must set the lower and upper bounds.");
                    }
                    else if (this.controller == null)
                    {
                        System.out.println("You must set up your ports first.\n");
                    }
                    else
                    {
                        // set up the job
                        System.out.print("Enter the number of exponents to check before a worker requests a new bounds range: ");
                        workChunkSize = (new Scanner(System.in)).nextInt();
                                                
                        byte[] payload    = new byte[collisionPingHeader.length];

                        for (int i = 0; i < collisionPingHeader.length; i++)
                        {
                            payload[i] = collisionPingHeader[i];
                        }

                        //for (int i = collisionPingHeader.length, j = -1; i < payload.length; i++)
                        //{
                        //  payload[i] = targetHashkey[++j];
                        //}

                        System.out.print("Would you also like to have this node work on it as well? (Y/n): ");
                        String thisIsWorker = new Scanner(System.in).nextLine();
                        isServer.getAndSet(true);
                        results = new LinkedList<Integer>();
                        int counter = 0;
                        for (Contact c : contactsList)
                        {
                            ++counter;
                            // send them an application ping
                            controller.sendPacket(payload, c.getInetAddress(), c.port);
                        }

                        System.out.println("Sent " + counter + " recruitment pings to:");
                        for (Contact c : contactsList)
                        {
                            System.out.println(c + "\n");
                        }

                        if (!thisIsWorker.equalsIgnoreCase("n"))
                        {
                            thisIsWorker();
                        }
                    }
                }
            }
            else if (input.equalsIgnoreCase("G"))
            {
                contactsList = loadContacts();
                System.out.println(contactsList.size() + " contacts fetched.\n");
            }
            else if (input.equalsIgnoreCase("E"))
            {
                if (controller == null)
                {
                    coreSettings      = new CoreConfig("settings", "contacts", -1, -1);
                    settings          = coreSettings.getSettings();
                    System.out.print("Enter outgoing port:\n> ");
                    outgoingPort      = in.nextInt();
                    controller = new SocketController(this, incomingPort, outgoingPort);

                }
                System.out.println("Incoming Port: " + incomingPort + "\n");
                System.out.println("Outgoing Port: " + outgoingPort + "\n");
            }
            else if (input.equalsIgnoreCase("S"))
            {
                if (!this.isServer.get())
                {
                    if (controller != null)
                    {
                        controller.closeSockets();
                        System.out.println("Shutting down sockets and exiting...");
                    }
                    return;
                }
                else
                {
                    int counter = 0;
                    for (Contact c : contactsList)
                    {
                        try
                        {
                            ++counter;
                            controller.sendPacket(this.collisionStopHeader, c.getInetAddress(), c.port);
                        } catch (IOException ex) {}
                    }

                    this.isServer.getAndSet(false);
                    this.first.getAndSet(true);
                    System.out.println("Sent stop requests to " + counter + " nodes.");
                }
            }
            else if (input.equalsIgnoreCase("R"))
            {                
                Scanner scan;

                do
                {
                    scan     = new Scanner(System.in);
                    System.out.println("Please enter the initial lower bound (Odd Exponent >= 0)");
                    initialRange[LOWER] = scan.nextInt();
                } while (isEven(initialRange[LOWER]) || (initialRange[LOWER] < 0));
                do
                {
                    scan     = new Scanner(System.in);
                    System.out.println("Please enter the max upper bound (Odd Exponent >= lower bound)");
                    initialRange[UPPER] = scan.nextInt();
                } while (isEven(initialRange[UPPER]) || (initialRange[UPPER] < initialRange[LOWER]));
            }

            else
            {
                    //System.out.println("Command not found.");
            }

            System.out.println();

        }
    }

    void thisIsWorker()
    {
        System.out.println("Not implemented.");
    }

    ArrayList<Contact> loadContacts()
    {

        ArrayList<Contact> result = new ArrayList<Contact>();
        try
        {
                final FileInputStream   fis          = new FileInputStream("contacts");
                final ObjectInputStream ois          = new ObjectInputStream(fis);
                final String            akb          = ois.readUTF();
                final String[]          kbuckets;
                final int               id           = 0;
                final int               address      = 1;
                final int               port         = 2;

                      String[]          contacts;

                ois.close();
                fis.close();
                kbuckets = akb.split(DHT.SEPERATOR);
                for (String k : kbuckets)
                {
                    contacts = k.split(KBucket.SEPERATOR);
                    for (String c : contacts)
                    {
                        //System.out.println(c);
                        String[]  s = c.split(Contact.SEPERATOR);
                        Contact old = new Contact(new UniqueId(s[id]),
                                      InetAddress.getByName(s[address]).getAddress(),
                                      Integer.parseInt(s[port]));

                       // dht.updateOrAddNew(old);
                        result.add(old);
                    }
                }
                return result;
        }
        catch (FileNotFoundException ex) {return result;}
        catch (IOException ex) {return result;}
    }


    public static void main (String[] args) throws Exception
    {



        MPrimeFinder mpf    = new MPrimeFinder();

        mpf.commandLine();
        return;

    }

    public LinkedList<Integer> newRound(int[] bounds)
    {
        LinkedList<Integer> result = new LinkedList<Integer>();
        int lowerBound = bounds[LOWER];
        int upperBound = bounds[UPPER];
                


        for(int exp= lowerBound; exp <= upperBound; exp += 2)
        {
            //Exponent param, Mersenne Prime is 2^exp-1
            BigInteger mprime = BigInteger.ONE.shiftLeft(exp).subtract(BigInteger.ONE);
            BigInteger lucas  = BigIntegerFOUR;//Primality Test constant 4

            for (int j = 3; j <= exp; j++)
            {
                lucas = lucas.multiply(lucas).subtract(BigInteger.valueOf(2)).mod(mprime);
                if(lucas.equals(BigInteger.ZERO))
                {
                    result.add(exp);
                    //System.out.println("2^"+exp+"-1  is Merssene prime") ;
                }
            }
        }
        
        return result;
    }

    public static int[] getNextBoundsRange(int[] lastBoundsRange, int maxUpperBound, int num)
    {
        int[] result = null;
        int nextLower = lastBoundsRange[UPPER] + 1;
        int nextUpper;

        if ((nextLower & 1) == 0) // if it's even
        {
            ++nextLower; // make it odd
        }

        if (nextLower <= maxUpperBound)
        {
            result = new int[2];

            result[LOWER] = nextLower;
            nextUpper = nextLower + num - 1;

            if ((nextUpper & 1) == 0) // if it's even
            {
               ++nextUpper; // make it odd
            }

            if (nextUpper > maxUpperBound)
            {
                nextUpper = maxUpperBound;
            }
            result[UPPER] = nextUpper;
        }

        return result;
    }

    public void processIncoming(DatagramPacket packet)
    {
       byte[] payload = packet.getData();
       switch (payload[0] & 0x0F)
       {
           case 1: // received ping
           {
               System.out.println("received MPrimePing (" + packet.getLength() + " bytes)");
               receivedPing(payload, packet.getLength());
               break;
           }
           case 2: // received workUnitRequest
           {
               System.out.println("received workUnitRequest (" + packet.getLength() + " bytes)");
               receivedWorkUnitRequest(payload, packet.getLength());
               break;
           }
           case 3: // received workUnit
           {
               System.out.println("received workUnit (" + packet.getLength() + " bytes)");
               receivedWorkUnit(payload, packet.getLength());
               break;
           }
           case 4: // received collision found
           {
               System.out.println("received collision found - NOT IMPLEMENTED (" + packet.getLength() + " bytes)");
             //  receivedCollisionFound(payload, packet.getLength());
               break;
           }
           case 8: // recieved stop
           {
               System.out.println("recieved stop (" + packet.getLength() + " bytes)");
               receivedStop(payload);
               break;
           }
           default:
               return;
       }
    }

    void receivedPing(byte[] payload, int length)
    {
        if (isWorking.compareAndSet(false, true))
        {
            // atleast get length of targetHashkey                      
            // send workunit request
            Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);

            try
            {
                System.out.println("Sending initial work unit request to: " + c.getInetAddress() + "/" + c.port);
                controller.sendPacket(this.workUnitRequestHeader, c.getInetAddress(), c.port);
            } catch (IOException ex) {}
        }
    }

    void receivedWorkUnitRequest(byte[] payload, int packetLength)
    {
        if (isServer.get())
        {         
            int[] nextBoundRange;

            if (first.compareAndSet(true, false))
            {
                synchronized (rangeBoundLock)
                {
                    int temp1 = initialRange[UPPER] - initialRange[LOWER] + 1;

                    if (workChunkSize > temp1)
                    {
                        workChunkSize = temp1;
                    }
                    int temp2 = initialRange[LOWER] + workChunkSize - 1;
                    if ((temp2 & 1) == 0) // if it's even
                    {
                        ++temp2; // make it odd
                    }
                    nextBoundRange = currentRange = new int[] { initialRange[LOWER], temp2 };
                }
            }
            else
            {
                int numResults = (packetLength - RPC.HEADER_LENGTH) / (Integer.SIZE / Byte.SIZE);

                int offset = RPC.HEADER_LENGTH;
                for (int i = 0; i < numResults; i++)
                {
                    int temp = ((payload[offset++] & 0xFF) << 24)
                    + ((payload[offset++] & 0xFF) << 16)
                    + ((payload[offset++] & 0xFF) <<  8)
                    +  (payload[offset++] & 0xFF);

                    //System.out.println(temp);
                    results.add(new Integer(temp));
                }

                synchronized (rangeBoundLock)
                {
                    nextBoundRange = currentRange = MPrimeFinder.getNextBoundsRange(currentRange.clone(), initialRange[UPPER], workChunkSize);
                }
            }

            // if nextBoundRange == null, we're done
            if (nextBoundRange == null)
            {
                int counter = 0;
                for (Contact c : contactsList)
                {
                    try
                    {
                        ++counter;
                        controller.sendPacket(this.collisionStopHeader, c.getInetAddress(), c.port);
                    } catch (IOException ex) {}
                }
                this.isServer.getAndSet(false);
                this.first.getAndSet(true);

                Collections.sort(results);
                System.out.println("Mersenne Primes Found:\n");
                for (Integer i : results)
                {
                    System.out.println("(2^" + i + ") - 1\n");
                }

                return;
            }
            Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);

            // send header + nextBoundRange
            payload = new byte[RPC.HEADER_LENGTH + (nextBoundRange.length * (Integer.SIZE / Byte.SIZE))];

            for (int i = 0; i < RPC.HEADER_LENGTH; ++i)
            {
                payload[i] = this.mPrimeWorkUnitHeader[i];
            }

            insertBoundsIntoPayload(nextBoundRange, payload);

            // send workunit
            System.out.println("Sent workunit packet (" + payload.length + " bytes)");
            System.out.println("Bounds range contents: " + nextBoundRange[LOWER] + " - " + nextBoundRange[UPPER]);

            try
            {
                controller.sendPacket(payload, c.getInetAddress(), c.port);
            } catch (IOException ex) {}
        }
    }

    void receivedWorkUnit(byte[] payload, int length)
    {
        if (isWorking.get())
        {
            int[] bounds = new int[2];
           
            bounds[LOWER] = ((payload[RPC.HEADER_LENGTH    ] & 0xFF) << 24) +
                        ((payload[RPC.HEADER_LENGTH + 1] & 0xFF) << 16) +
                        ((payload[RPC.HEADER_LENGTH + 2] & 0xFF) << 8 ) +
                         (payload[RPC.HEADER_LENGTH + 3] & 0xFF);

            bounds[UPPER] = ((payload[RPC.HEADER_LENGTH + 4    ] & 0xFF) << 24) +
                        ((payload[RPC.HEADER_LENGTH + 5] & 0xFF) << 16) +
                        ((payload[RPC.HEADER_LENGTH + 6] & 0xFF) << 8 ) +
                         (payload[RPC.HEADER_LENGTH + 7] & 0xFF);

     

            // start working on it

            // send workunit request when done, unless told to stop
            System.out.print("Bounds Range:  "+ bounds[LOWER] + " - " + bounds[UPPER]);
            
            LinkedList<Integer> result = newRound(bounds);

            if (isWorking.get())
            {
                Contact c = new Contact(payload, RPC.SOURCE_ADDRESS_OFFSET);
                payload = new byte[RPC.HEADER_LENGTH + (result.size() * 4)];

                for (int i = 0; i < RPC.HEADER_LENGTH; i++)
                {
                    payload[i] = this.workUnitRequestHeader[i];
                }
                insertResultsIntoPayload(result, payload);
                try
                {
                    controller.sendPacket(payload, c.getInetAddress(), c.port);
                } catch (IOException ex) {}
             }            
         }        
    }

    void receivedStop(byte[] payload)
    {
        if (isWorking.get())
        {
            isWorking.getAndSet(false);
            // stop working
        }
    }

    boolean isEven(int num)
    {
        return ((num & 1) == 0);
    }

    static void insertBoundsIntoPayload(int[] boundRange, byte[] payload)
    {

         int offset = payload.length - 8;

         payload[offset++] = (byte)(boundRange[LOWER] >>> 24);
         payload[offset++] = (byte)(boundRange[LOWER] >>> 16);
         payload[offset++] = (byte)(boundRange[LOWER] >>> 8);
         payload[offset++] = (byte)(boundRange[LOWER]);
         payload[offset++] = (byte)(boundRange[UPPER] >>> 24);
         payload[offset++] = (byte)(boundRange[UPPER] >>> 16);
         payload[offset++] = (byte)(boundRange[UPPER] >>> 8);
         payload[offset++] = (byte)(boundRange[UPPER]);
    }

    static void insertResultsIntoPayload(LinkedList<Integer> list, byte[] payload)
    {
         int offset = RPC.HEADER_LENGTH;

         for (Integer i : list)
         {
            payload[offset++] = (byte)(i.intValue() >>> 24);
            payload[offset++] = (byte)(i.intValue() >>> 16);
            payload[offset++] = (byte)(i.intValue() >>> 8);
            payload[offset++] = (byte)(i.intValue());
         }
    }

    // testing only
    static int[] bytesToInts(byte[] bytes)
    {
        int[] result = new int[2];
        int offset = bytes.length - 8;

        result[0] = ((bytes[offset++] & 0xFF) << 24)
                    + ((bytes[offset++] & 0xFF) << 16)
                    + ((bytes[offset++] & 0xFF) <<  8)
                    +  (bytes[offset++] & 0xFF);
        result[1] = ((bytes[offset++] & 0xFF) << 24)
                    + ((bytes[offset++] & 0xFF) << 16)
                    + ((bytes[offset++] & 0xFF) <<  8)
                    +  (bytes[offset++] & 0xFF);
        return result;
    }
}